using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Linq;


namespace GuavaData
{
    public class Apply : Transform
    {
        Function f;
        string table;
        List<string> inColumns, outColumns;
        Dictionary<string, Domain.Domain> inputColumns;

        public Apply(Function f, string table, List<string> inNames, List<string> outNames)
        {
            this.f = f;
            this.table = table;
            this.inputColumns = null;
            this.inColumns = inNames;
            this.outColumns = outNames;
            if (f.inputs.Count != inNames.Count) throw new TransformException("Apply operator has been provided a function with " + f.inputs.Count.ToString() + " inputs and only " + inNames.Count.ToString() + " column names.");
            if (f.outputs.Count != outNames.Count) throw new TransformException("Apply operator has been provided a function with " + f.outputs.Count.ToString() + " outputs and only " + outNames.Count.ToString() + " column names.");
            if (!(f.IsInvertible)) throw new TransformException("Apply operator can only work with an invertible function.");
        }
        internal override void Expand(AddColumn ac, Command c)
        {
            if (ac.Table != table)
                return;
            if (this.inputColumns.ContainsKey(ac.Column))
                throw new Exception("Column " + ac.Column + " already exists in table " + ac.Table + ".");
            else
            {
                inputColumns.Add(ac.Column, ac.Domain);
            }
        }

        internal override void Expand(AlterColumnRename acr, Command c)
        {
            if (acr.Table != table)
                return;
            if (!this.inputColumns.ContainsKey(acr.OldName))
                throw new Exception("Column " + acr.NewName + " does not exist in table " + acr.Table + ", cant rename.");

            if (this.inputColumns.ContainsKey(acr.NewName))
                throw new Exception("Column " + acr.NewName + " already exists in table " + acr.Table + ", cant rename to this.");

            inputColumns.Add(acr.NewName, inputColumns[acr.OldName]);
            inputColumns.Remove(acr.OldName);

            if (inColumns.Contains(acr.OldName))
                inColumns[inColumns.IndexOf(acr.OldName)] = acr.NewName;
        }

        internal override void Expand(DropColumn dc, Command c)
        {
            if (dc.Table != table)
                return;
            if (inColumns.Contains(dc.Column))
                throw new Exception("Cannot drop column " + dc.Column + " in table " + table + ", because a function is defined using it.");
            if (!inputColumns.ContainsKey(dc.Column))
                throw new Exception("Cannot drop column " + dc.Column + " as it does not exist in table " + dc.Table + ".");
            else
                inputColumns.Remove(dc.Column);
        }

        internal override void Expand(AddElement ae, Command c)
        {
            if (ae.Table != table)
                return;
            if (!(inputColumns.ContainsKey(ae.Column)))
                throw new Exception("Column " + ae.Column + " does not exist in table " + ae.Table + ", unable to add element " + ae.Element + ".");
            if (inColumns.Contains(ae.Column))
                throw new Exception("Column " + ae.Column + " is used in a function which is not defined for the new domain " + ae.Element + ".");

            Domain.Enumerated e = this.inputColumns[ae.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(ae.Element);
            if (i > 0)
                throw new Exception("Domain already contains the element " + ae.Element + ".");

            e.Items.Add(ae.Element);
            inputColumns[ae.Column] = e;


        }

        internal override void Expand(AlterElementRename aer, Command c)
        {
            if (aer.Table != table)
                return;
            if (!(inputColumns.ContainsKey(aer.Column)))
                throw new Exception("Column " + aer.Column + " does not exist in table " + aer.Table + ", unable to change element " + aer.OldName + ".");
            if (inColumns.Contains(aer.Column))
                throw new Exception("Column " + aer.Column + " is used in a function which is not defined for the new domain " + aer.NewName + ".");

            Domain.Enumerated e = this.inputColumns[aer.Column] as Domain.Enumerated;
            if (e.Items.IndexOf(aer.NewName) > 0)
                throw new Exception("Column " + aer.Column + " already contains " + aer.NewName + " in it's domain, cant rename to that name.");
            int i = e.Items.IndexOf(aer.OldName);
            if (i == 0)
                throw new Exception("Domain for column " + aer.Column + " does not contain the element " + aer.OldName + ", cannot rename.");

            e.Items[i] = aer.NewName;
            inputColumns[aer.Column] = e;

        }

        internal override void Expand(DropElement de, Command c)
        {
            if (de.Table != table)
                return;
            if (!(inputColumns.ContainsKey(de.Column)))
                throw new Exception("Column " + de.Column + " does not exist in table " + de.Table + ", unable to delete element " + de.Element + " from domain.");
            if (inColumns.Contains(de.Column))
                throw new Exception("Column " + de.Column + " is used in a function which is not defined if element, " + de.Element + ", is removed.");

            Domain.Enumerated e = this.inputColumns[de.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(de.Element);
            if (i == 0)
                throw new Exception("Domain of column " + de.Column + " does not contain the element " + de.Element + ", cannot remove.");

            e.Items.Remove(de.Element);
            inputColumns[de.Column] = e;
        }

        internal override void Expand(AddTable at, Command c)
        {
            if (at.Table != table)
                return;

            inputColumns = new Dictionary<string, GuavaData.Domain.Domain>();
            for (int i = 0; i < at.Columns.Count; i++)
                inputColumns.Add(at.Columns[i], at.Domains[i]);

            List<string> cols = at.Columns;
            List<GuavaData.Domain.Domain> doms = at.Domains;
            foreach (string inc in inColumns)
            {
                if (cols.Contains(inc))
                {
                    doms.RemoveAt(cols.IndexOf(inc));
                    cols.Remove(inc);
                }
            }
            for (int i = 0; i < outColumns.Count; i++)
            {
                //if (cols.Contains(outColumns[i]))
                //    throw new Exception("Column, " + outColumns[i] + ", which is the output column for a function already exists in the table, " + at.Table + ".");
                cols.Add(outColumns[i]);
                doms.Add(f.outputs[i]);
            }

            at.Columns = cols;
            at.Domains = doms;
        }

        internal override void Expand(AlterTableRename atr, Command c)
        {
            if (atr.OldName != table)
                return;
            else
                table = atr.NewName;
        }

        internal override void Expand(DropTable dt, Command c)
        {
            if (dt.Table != table)
                return;

            inputColumns = null;
        }

        internal override void Expand(Delete d, Command c)
        {
        }

        internal override void Expand(ForeignKey fk, Command c)
        {
            if (fk.ChildTable != table)
                return;

            foreach (string s in inColumns)
            {
                if (fk.ChildColumns.Contains(s))
                {
                    c.NotifyChange();
                    return;
                }
            }

        }

        internal override void Expand(Insert i, Command c)
        {
            if (i.Table != table)
                return;

            i.Child.Visit(this, c);

            Boolean hasInCols = false;
            List<string> cols = new List<string>();
            if (i.Columns != null)
            {
                foreach (String s in i.Columns)
                {
                    if (!inColumns.Contains(s))
                        cols.Add(s);
                    else
                        hasInCols = true;
                }
                if (hasInCols)
                {
                    foreach (String str in outColumns)
                    {
                        if (cols.Contains(str))
                        {
                            throw new Exception("Column, " + str + ", which is the output column for a function is used as an input column.");
                        }
                        cols.Add(str);
                    }
                }

                c.NotifyChange(new Command(new Insert(table, cols, new ApplyOp(i.Child, inColumns, outColumns, f))));
            }
        }

        internal override void Expand(TableRef tr, Command c)
        {
            if (tr.Table != table)
                return;
            TableRef newTr = new TableRef(table, tr.Alias);
            foreach (string s in tr.Columns)
                if (!inColumns.Contains(s))
                    newTr.AddColumn(s, tr.exposedColumns[tr.Columns.IndexOf(s)], tr.exposedDomains[tr.Columns.IndexOf(s)]);
            for (int i = 0; i < outColumns.Count; i++)
                newTr.AddColumn(outColumns[i], outColumns[i], f.outputs[i]);
            List<String> newInColumns = new List<string>();
            for (int i = 0; i < inColumns.Count; i++)
                newInColumns.Add(tr.Alias + "_" + inColumns[i]);
            ApplyOp ap = new ApplyOp(newTr, outColumns, newInColumns, f.Inverse);
            tr.parent.NotifyChange(tr, ap);
        }

        internal override void Expand(Update u, Command c)
        {
            //not our table, return
            if (u.Table != table)
                return;

            u.Child.Visit(this, c);
            Boolean hasInCols = false;
            Boolean hasAllInCols = true;
            List<string> cols = new List<string>();// u.Child.exposedColumns;
            List<GuavaData.Domain.Domain> doms = new List<GuavaData.Domain.Domain>();// u.Child.exposedDomains;
            //create a list of columns that doesnt contain the input columns for the function
            foreach (String s in u.Child.exposedColumns)
            {
                if (inColumns.Contains(s))
                {
                    hasInCols = true;
                }
                else
                {
                    cols.Add(s);
                    doms.Add(u.Child.exposedDomains[u.Child.exposedColumns.IndexOf(s)]);
                }
            }
            //if there are no function columns in the update return
            if (!hasInCols)
                return;

            else
            {
                // determine if all the input columns are there
                foreach (string s in inColumns)
                    if (!u.Child.exposedColumns.Contains(s))
                    {
                        hasAllInCols = false;
                        break;
                    }
                //add function output columns to the column list
                foreach (String str in outColumns)
                {
                    if (cols.Contains(str))
                    {
                        throw new Exception("Column, " + str + ", which is the output column for a function is used as an input column.");
                    }
                    cols.Add(str);
                    doms.Add(new GuavaData.Domain.String());
                }
                //if all the funtion input columns are being changed just apply the funtion and update
                if (hasAllInCols)
                {
                    c.NotifyChange(new Command(new Update(table, u.CondColumns, u.Conditions, cols, new ApplyOp(u.Child, inColumns, outColumns, f))));
                }
                else
                {
                    //need to retrive existing values that arent being changed
                    TableRef tr = new TableRef(u.Table, c.nextTableAlias(u.Table));
                    foreach (string col in inColumns)
                        tr.AddColumn(col, col, inputColumns[col]);
                    Filter fil = new Filter(tr, u.CondColumns, u.Conditions);
                    ApplyOp inverse = new ApplyOp(fil, outColumns, inColumns, f.Inverse);

                    List<string> oldFunctionColumns = new List<string>();
                    foreach (String s in inColumns)
                    {
                        if (!(u.Child.exposedColumns.Contains(s)))
                            oldFunctionColumns.Add(s);
                    }
                    Project proj = new Project(fil, oldFunctionColumns);

                    //now cross join existing funtion values with the new ones to update and apply fucntion
                    CrossJoin cross = new CrossJoin(u.Child, proj);
                    ApplyOp apply = new ApplyOp(cross, inColumns, outColumns, f);

                    c.NotifyChange(new Command(new Update(table, u.CondColumns, u.Conditions, cols, apply)));
                }
            }
        }
        public override GuavaData.Transform Inverse()
        {
            Apply a = new Apply(f.Inverse, table, outColumns, inColumns);
            return a;
        }
        public override List<XElement> ToXElement()
        {
            XElement x = new XElement("Transform", new XAttribute("type", "Apply"),
                new XAttribute("table", table),f.ToXElement());
            XElement inCols = new XElement("inCols");
            for (int i = 0; i < inColumns.Count; i++)
                inCols.Add(new XElement(inColumns[i]));
            XElement outCols = new XElement("outCols");
            for (int i = 0; i < outColumns.Count; i++)
                outCols.Add(new XElement(outColumns[i]));
            x.Add(inCols, outCols);

            List<XElement> xList = new List<XElement>();
            if (!(this.Child is SqlProvider))
                xList = ((Transform)this.Child).ToXElement();
            xList.Add(x);
            return xList;
            
        }
        new public static Apply FromXElement(XElement x)
        {
            string table = x.Attribute("table").Value;
            if (!x.HasElements)
                throw new Exception("While retreiving Apply transfrom from xml: no child elements were found");


            List<string> inCols = new List<string>();
            List<string> outCols = new List<string>();
            Function f = null;
            foreach (XElement xe in x.Elements())
            {
                if (xe.Name.LocalName == "Function")
                {
                    // What type should the new function be?
                    var type = xe.Attribute("type").Value;

                    if (type == "GuavaData.Function.BitMask")
                    {
                        f = new BitMask(int.Parse(xe.Elements().First().Value.ToString()));
                    }
                    else if (type == "GuavaData.Function.ReverseBitMask")
                    {
                        f = new ReverseBitMask(int.Parse(xe.Elements().First().Name.LocalName));
                    }
                    else if (type == "GuavaData.Function.ColumnConcatenation")
                    {
                        List<string> paramList = new List<string>();
                        foreach (var param in xe.Elements()) paramList.Add(param.Value.ToString());
                        f = new ColumnConcatenation(paramList);
                    }
                    else if (type == "GuavaData.Function.ColumnConcatenationInverse")
                    {
                        List<string> paramList = new List<string>();
                        foreach (var param in xe.Elements()) paramList.Add(param.Value.ToString());
                        f = new ColumnConcatenationInverse(paramList);
                    }
                    else throw new Exception("While retreiving Apply transfrom from xml: unreconized function was found");
                }
                if (xe.Name.LocalName == "inCols")
                {
                    foreach (var col in xe.Elements()) inCols.Add(col.Name.LocalName);
                }
                if (xe.Name.LocalName == "outCols")
                {
                    foreach (var col in xe.Elements()) outCols.Add(col.Name.LocalName);
                }
            }
            if (f == null)
                throw new Exception("While retreiving Apply transfrom from xml: no element named Function was found");


            return new Apply(f, table, inCols, outCols);
        }
    }
}